﻿using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;

using NATP2P.STUN;

namespace NATP2P.UDPNat
{
    /// <summary>
    /// type of nat 
    /// </summary>
    public enum TypeNat
    {
        /// <summary>
        /// 
        /// </summary>
        FullCone,
        /// <summary>
        /// 
        /// </summary>
        RestrictedCone,
        /// <summary>
        /// 
        /// </summary>
        PortRestricted,
        /// <summary>
        /// 
        /// </summary>
        Symmetric

    }
    /// <summary>
    /// This class represent socket connect through the nat 
    /// The connection must be  almost at the same time  using rendezvous server or exchange data public ip and port
    /// </summary>
    public class UDPNatSocket : IUDPNatSocket, IDisposable
    {
        

        private Socket _ClientSocket;
        private Socket _ServerSocket;
        private Thread _KeepAliveThread;
        private IEndPointMapRequest _endpointmap;
        private TypeNat _typeRNat;
        private readonly object sockeSendtlock = new object();

        /// <summary>
        /// Occurs when the new data receiving from remote host 
        /// </summary>
        public event EventHandler<NewDatagramEventArgs> RaiseNewDatagramEvent;


        private EndPoint remoteEP = (EndPoint)(new IPEndPoint(IPAddress.Any, 0));

        /// <summary>
        /// Initializes a new instance of the <see cref="T:NATP2P.UdpNat.UDPNatSocket" /> class using the specified InternalEndPoint and RemoteEndPoint.
        /// use this instance of the class with Connect Function
        /// </summary>
        /// <param name="endpointmap">EndPointMapRequest <see cref="T:NATP2P.STUN.EndPointMapRequest" /></param>
        /// <param name="RemoteEndPoint">RemoteEndPoint <see cref="T:System.Net.IPEndPoint" /></param>
        /// <param name="typeRNat"></param>
        public UDPNatSocket(IEndPointMapRequest endpointmap, IPEndPoint RemoteEndPoint, TypeNat typeRNat)
        {
            if ((endpointmap == null) || (RemoteEndPoint == null))
            {
                throw new ArgumentNullException("paramater is null");
            }
            this._endpointmap = endpointmap;
            this._endpointmap.Send();
            this._internalEndPoint = endpointmap.InternalEndPoint;
            this._externalEndPoint = endpointmap.ExternalEndPoint;
            this._remoteEndPoint = RemoteEndPoint;
            this._typeRNat = typeRNat;
            this._connected = false;


        }
        /// <summary>
        /// Initializes a new instance of the <see cref="T:NATP2P.UdpNat.UDPNatSocket" /> class using the specified InternalEndPoint .
        /// use this instance of the class with ConnectTo Function
        /// </summary>
        /// <param name="endpointmap">EndPointMapRequest <see cref="T:NATP2P.STUN.EndPointMapRequest" /></param>
        /// <param name="typeRNat"></param>
        public UDPNatSocket(IEndPointMapRequest endpointmap, TypeNat typeRNat)
        {
            if (endpointmap == null)
            {
                throw new ArgumentNullException("paramater is null");
            }
            this._endpointmap = endpointmap;
            this._endpointmap.Send();
            this._internalEndPoint = endpointmap.InternalEndPoint;
            this._externalEndPoint = endpointmap.ExternalEndPoint;
            this._typeRNat = typeRNat;
            this._connected = false;
        }


        // sending empty packet for every 30 second to keep the hole open
        //statistic not specified  to exactly 30 second(depend on router how it's configurated) but for ensuring 
        private void KeepAlive()
        {
            byte[] emptyPacket = new byte[1] { 0 };

            while (true)
            {

                lock (sockeSendtlock)
                {
                    _ClientSocket.BeginSendTo(emptyPacket, 0, emptyPacket.Length, SocketFlags.None, RemoteEndPoint, new AsyncCallback(EndKeepAlive), _ClientSocket);
                }
                Thread.Sleep(30000);

                //configurate timer for 30 second
            }
        }

        private void EndKeepAlive(IAsyncResult so)
        {
            _ClientSocket.EndSendTo(so);
        }

        //setup socket and specify socket option for port reusing 
        private void InitiateSocket()
        {
            _ServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            _ServerSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            _ServerSocket.ExclusiveAddressUse = false;
            _ServerSocket.Bind(InternalEndPoint);
            _ClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            _ClientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            _ClientSocket.ExclusiveAddressUse = false;
            _ClientSocket.Bind(InternalEndPoint);
           
        }


        /// <param name="e">An <see cref="T:NATP2P.UDPNatSocket.NewDatagramEventArgs" /> that contains the event data.</param>
        protected virtual void OnRaiseDatagramEvent(NewDatagramEventArgs e)
        {
            // Make a temporary copy of the event to avoid possibility of
            // a race condition if the last subscriber unsubscribes
            // immediately after the null check and before the event is raised.
            EventHandler<NewDatagramEventArgs> handler = RaiseNewDatagramEvent;

            // Event will be null if there are no subscribers
            if (handler != null)
            {
                // Use the () operator to raise the event.
                handler(this, e);
            }
        }

        /// <summary>
        /// Connect to remote host
        /// </summary>
        public void Connect()
        {

                HolePunchPeer hole = new HolePunchPeer(InternalEndPoint, RemoteEndPoint, _typeRNat);
                hole.RaiseStateChangedEvent += new EventHandler<StateChangedEventArgs>(hole_RaiseStateChangedEvent);
                ThreadPool.QueueUserWorkItem(hole.PunchHole);

        }
        /// <summary>
        /// listen for incoming connection
        /// </summary>
        public void Listen()
        {
            HolePunchPeer hole = new HolePunchPeer(InternalEndPoint, RemoteEndPoint, _typeRNat);
            hole.RaiseStateChangedEvent += new EventHandler<StateChangedEventArgs>(hole_RaiseStateChangedEvent);
            ThreadPool.QueueUserWorkItem(hole.KeepHoleAlive);
        }
        void hole_RaiseStateChangedEvent(object sender, StateChangedEventArgs e)
        {
           
           
            HolePunchPeer peer = sender as HolePunchPeer;
            peer.Close();
            this.Connected = true;
            this.RemoteEndPoint = e.RemoteEndPoint;
            InitiateSocket();
            _KeepAliveThread = new Thread(KeepAlive);
            _KeepAliveThread.IsBackground = true;
            _KeepAliveThread.Start();
            this.Receive();
           //
        }
        /// <summary>
        /// connect to remote host  
        /// </summary>
        /// <param name="RemoteEndPoint">use remote endpoint <see cref="T:System.Net.IPEndPoint" /></param>
        public void ConnectTo(IPEndPoint RemoteEndPoint)
        {
            this.RemoteEndPoint = RemoteEndPoint;
            InitiateSocket();
            _KeepAliveThread = new Thread(KeepAlive);
            _KeepAliveThread.IsBackground = true;
            _KeepAliveThread.Start();
        }

        /// <summary>
        /// Sends data to a connected <see cref="T:NATP2P.UdpNat.UDPNatSocket" />.
        /// </summary>
        /// <param name="buffer">An array of type <see cref="T:System.Byte" /> that contains the data to be sent.</param>
        public void Send(byte[] buffer)
        {
            lock (sockeSendtlock)
            {
                _ClientSocket.BeginSendTo(buffer, 0, buffer.Length, SocketFlags.None, RemoteEndPoint, new AsyncCallback(End_Send), _ClientSocket);
            }
        }
        private void End_Send(IAsyncResult so)
        {
            _ClientSocket.EndSendTo(so);
        }


        byte[] _buffer = new byte[65536];
        /// <summary>
        /// Receives a datagram into the data buffer and stores the endpoint.
        /// </summary>
        public void Receive()
        {
            if(_ServerSocket!=null)
            _ServerSocket.BeginReceiveFrom(_buffer, 0, _buffer.Length, SocketFlags.None, ref remoteEP, new AsyncCallback(End_Receive), _ServerSocket);
        }
        NATHeader natHeader = new NATHeader();
        private void End_Receive(IAsyncResult so)
        {

            int num = _ServerSocket.EndReceiveFrom(so, ref remoteEP);
            natHeader.Parse(_buffer);
            if (natHeader.MessageType == (int)MesgType.BindingConnect)
            {
                byte[] d = natHeader.ToByte((int)MesgType.BindingAccpt, false, 0, natHeader.Port);
                _ClientSocket.BeginSendTo(d, 0, d.Length, SocketFlags.None, RemoteEndPoint, new AsyncCallback(End_Send), _ClientSocket);
            }
            if (num != 1)// to do: remove header of the packet 4 byte
                if (num < 65536)
                {
                    byte[] array = new byte[num];
                    Buffer.BlockCopy(this._buffer, 0, array, 0, num);
                    OnRaiseDatagramEvent(new NewDatagramEventArgs(array));
                }
                else { OnRaiseDatagramEvent(new NewDatagramEventArgs(_buffer)); }


            _ServerSocket.BeginReceiveFrom(_buffer, 0, _buffer.Length, SocketFlags.None, ref remoteEP, new AsyncCallback(End_Receive), _ServerSocket);
        }


        private IPEndPoint _internalEndPoint;
        /// <summary>
        /// get local ip address and port
        /// </summary>
        /// <exception cref="T:NATP2P.NATExceptionHandler.PropertyNullException">An error occurred when attempting to access the property. See the Remarks section for more information. </exception>
        public IPEndPoint InternalEndPoint
        {
            get
            {
                return _internalEndPoint;
            }
            private set
            {
                _internalEndPoint = value;
            }
        }

        private IPEndPoint _externalEndPoint;
        /// <summary>
        /// get public ip address and port
        /// </summary>
        /// <exception cref="T:NATP2P.NATExceptionHandler.PropertyNullException">An error occurred when attempting to access the property. See the Remarks section for more information. </exception>
        public IPEndPoint ExternalEndPoint
        {
            get
            {
                return _externalEndPoint;
            }
            private set
            {
                _externalEndPoint = value;
            }
        }

        private IPEndPoint _remoteEndPoint;
        /// <summary>
        /// get remote ip and port belong to remote host
        /// </summary>
        /// <exception cref="T:NATP2P.NATExceptionHandler.PropertyNullException">An error occurred when attempting to access the property. See the Remarks section for more information. </exception>
        public IPEndPoint RemoteEndPoint
        {
            get
            {
                return _remoteEndPoint;
            }
            private set
            {
                _remoteEndPoint = value;
            }
        }

        bool _connected;
        /// <summary>
        /// 
        /// </summary>
        public bool Connected
        {
            get
            {
                return _connected;
            }
            private set
            {
                _connected = value;
            }
        }

        private bool disposed = false;
        /// <summary>
        /// Releases all resources used by the current instance of the <see cref="T:NATP2P.UdpNat.UDPNatSocket" /> class.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        private void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called.
            if (!this.disposed)
            {
                // If disposing equals true, dispose all managed
                // and unmanaged resources.
                if (disposing)
                {
                    // Dispose managed resources.
                    if (_KeepAliveThread.IsAlive)
                        _KeepAliveThread.Abort();
                    if (this._ServerSocket != null)
                        this._ServerSocket.Close();
                    if (this._ClientSocket != null)
                        this._ClientSocket.Close();

                }

                // Note disposing has been done.
                disposed = true;

            }
        }

        /// <summary>
        /// Releases all resources used by the current instance of the <see cref="T:NATP2P.UdpNat.UDPNatSocket" /> class.
        /// </summary>
        public void Close()
        {
            this.Dispose();
        }






    }
}
